Skip to content

fix(discovery): sort discovered addresses by connectability - #37

Merged
Developer1010x merged 1 commit into
masterfrom
fix/receiver-address-selection
Aug 26, 2026
Merged

fix(discovery): sort discovered addresses by connectability#37
Developer1010x merged 1 commit into
masterfrom
fix/receiver-address-selection

Conversation

@snadahalli

Copy link
Copy Markdown
Owner

The bug

mdns_sd::ServiceInfo::get_addresses returns a &HashSet<IpAddr>, and all three browsers collected it straight into a Vec:

addresses: info.get_addresses().iter().copied().collect(),

Callers then dial addresses.first(). So the address actually connected to was an arbitrary hash-set element — not merely unsorted but non-deterministic, and re-rolled every time a record refreshed.

Observed across a single run, same machine, same service:

[192.168.0.97, fe80::1427:c2c8:a235:a115]
[fe80::1, ::1, fe80::1427:c2c8:a235:a115, 127.0.0.1, 192.168.0.97]
[127.0.0.1, ::1, fe80::1, 192.168.0.97, fe80::1427:c2c8:a235:a115]
[::1, fe80::1, 127.0.0.1]

first() yielded 192.168.0.97, fe80::1, 127.0.0.1 or ::1 depending on timing, and the fourth record offered no routable address at all. Remote receivers advertise only two addresses so they usually looked fine, which is what kept this hidden — a machine advertising loopback and link-local alongside its LAN address exposes it immediately.

The fix

Sort at collection, most-connectable first:

Rank Kind Why
0 routable IPv4 needs no scope handling
1 routable IPv6
2 loopback resolves, but to the wrong host for a remote receiver
3 link-local not dialable — we carry no zone index and cannot reconstruct one
4 unspecified

Ties break on the address value, so the result is stable run to run in a way HashSet iteration order is not. Link-local ranks below loopback deliberately: fe80::1 without a zone cannot be connected to at all, whereas loopback at least resolves.

This makes addresses.first() correct by construction, so the three addr() arms in the sender need no change.

Ipv6Addr::is_unicast_link_local is still unstable, so the fe80::/10 test is done directly on the segments.

Verification

7 new unit tests, including one asserting every rotation of the observed address set produces the same first address — the whole point of the change.

Confirmed against the live network: every discovered receiver now reports a routable IPv4 first, on every refresh.

fmt --check clean, clippy --all-targets --all-features -D warnings clean, cargo test --all 214 passed / 0 failed on macOS arm64.

`mdns_sd::ServiceInfo::get_addresses` returns a `&HashSet<IpAddr>`, and all
three browsers collected it straight into a `Vec` with
`.iter().copied().collect()`. Callers then dial `addresses.first()`, so the
address actually connected to was an arbitrary hash-set element — not merely
unsorted but non-deterministic, and re-rolled every time a record refreshed.

Observed across a single run, same machine, same service:

    [192.168.0.97, fe80::1427:c2c8:a235:a115]
    [fe80::1, ::1, fe80::1427:c2c8:a235:a115, 127.0.0.1, 192.168.0.97]
    [127.0.0.1, ::1, fe80::1, 192.168.0.97, fe80::1427:c2c8:a235:a115]
    [::1, fe80::1, 127.0.0.1]

So `first()` yielded 192.168.0.97, fe80::1, 127.0.0.1 or ::1 depending on
timing, and the fourth record offered no routable address at all. Remote
receivers only advertise two addresses so they usually looked fine, which is
what kept this hidden; a machine advertising loopback and link-local
alongside its LAN address exposes it immediately.

Sort at collection instead, most-connectable first: routable IPv4, routable
IPv6, loopback, link-local, unspecified — ties broken on the address value so
the result is stable run to run. Link-local ranks below loopback because we
carry no zone index and cannot reconstruct one, so `fe80::1` is not dialable
at all, whereas loopback at least resolves (to the wrong host for a remote
receiver, hence still below anything routable). IPv4 is preferred over
routable IPv6 since it needs no scope handling.

This makes `addresses.first()` correct by construction, so the three `addr()`
arms in the sender need no change.

Verified against the live network: every discovered receiver now reports a
routable IPv4 first, on every refresh.
@snadahalli
snadahalli force-pushed the fix/receiver-address-selection branch from 711ee74 to 55a1f15 Compare August 26, 2026 09:38
@Developer1010x
Developer1010x merged commit bcfd086 into master Aug 26, 2026
5 checks passed
Developer1010x pushed a commit that referenced this pull request Aug 26, 2026
…43)

#42 was merged twelve seconds after #41, into `fix/airplay-hkp-header` —
which #41 had just merged into master and left behind. The merge succeeded,
so nothing looked wrong, but the commits landed on a branch nothing points at
and master never received them.

Master therefore has the `X-Apple-HKP` header from #41 and none of what it was
a prerequisite for: the SRP proof still hashes g padded, transient pairing
still runs M5/M6 and gets the connection closed, and there is no encrypted
control channel. Pairing is broken on master in exactly the way #42 fixed.

This restores #42's own diff — the eight `openplay-airplay` files it actually
touched — on top of current master.

Deliberately *not* a merge of `fix/airplay-hkp-header`. That branch was cut
before #35, #36, #37, #39 and #40 merged, so a diff against it reads as
deleting `openplay-discovery/src/address.rs` and reverting 244 lines of
`openplay-sender/src/app.rs`. Merging it would silently undo five landed
fixes. Only the range between #42 and its own parent is safe to replay, and
that range touches nothing outside `openplay-airplay`.

Verified after the replay: 233 tests pass, clippy and fmt clean, and the work
from #35/#36/#37/#40 is still in the tree.

Co-authored-by: Sandeepa Nadahalli <1698507+snadahalli@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants